package ru.myrtle.umf.util;

import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import ru.myrtle.umf.UAttribute;
import ru.myrtle.umf.UClass;
import ru.myrtle.umf.UFeature;
import ru.myrtle.umf.UModel;
import ru.myrtle.umf.UObject;
import ru.myrtle.umf.UReference;

public class UXMLWriter {

    private static final String ELEMENT_MODEL = "model";
    private static final String ELEMENT_OBJECT = "object";
    private static final String ELEMENT_OBJECT_REF = "object-ref";
    private static final String ELEMENT_ATTRIBUTE = "attribute";
    private static final String ELEMENT_REFERENCE = "reference";

    private static final String ATTRIBUTE_ID = "id";
    private static final String ATTRIBUTE_NAME = "name";
    private static final String ATTRIBUTE_PACKAGE = "package";
    private static final String ATTRIBUTE_TYPE = "type";

    /*
     *
     */

    private class Lookup implements Iterable<Map.Entry<UObject, String>> {

        private final Map<UObject, String> objects =
                new HashMap<UObject, String>();

        /*
         *
         */

        @Override
        public Iterator<Map.Entry<UObject, String>> iterator() {
            return objects.entrySet().iterator();
        }

        public String getObjectID(UObject object) {
            return objects.get(object);
        }

        /*
         *
         */

        public void build(UObject object) {
            processObject(object);
        }

        private void processObject(UObject object) {
            objects.put(
                    object, createObjectID()
            );

            for (UObject child : object.uContents()) {
                if (!objects.containsKey(child)) {
                    processObject(child);
                }
            }
        }

        private String createObjectID() {
            return UUID.randomUUID().toString();
        }
    }


    /*
     *
     */

    private Lookup lookup;
    private XMLStreamWriter writer;

    /*
     *
     */

    public void write(UObject object, Writer destination) throws UXMLException {
        lookup = new Lookup();
        lookup.build(object);

        try {
            writer = new UXMLStreamWriter(
                    XMLOutputFactory.newInstance().createXMLStreamWriter(
                            destination
                    )
            );

            writeObjects();
        }
        catch (XMLStreamException thrown) {
            throw new UXMLException(
                    thrown.getMessage(), thrown
            );
        }
    }

    void writeObjects() throws UXMLException, XMLStreamException {
        try {
            writer.writeStartDocument();
            writer.writeStartElement(ELEMENT_MODEL);

            for (Map.Entry<UObject, String> entry : lookup) {
                writeObject(
                        entry.getKey(), entry.getValue()
                );
            }

            writer.writeEndElement();
            writer.writeEndDocument();
        }
        finally {
            writer.close();
        }
    }

    void writeObject(UObject object, String objectID) throws UXMLException, XMLStreamException {
        UClass uClass = object.uClass();

        writer.writeStartElement(ELEMENT_OBJECT);
        writer.writeAttribute(ATTRIBUTE_ID, objectID);
        writer.writeAttribute(
                ATTRIBUTE_PACKAGE, uClass.getPackage().getNamespaceURI()
        );
        writer.writeAttribute(
                ATTRIBUTE_TYPE, uClass.getName()
        );

        for (UFeature feature : uClass.getAllFeatures()) {
            if (feature.isTransient() || feature.isVolatile() || feature.isDerived()) {
                continue;
            }

            if (feature.isChangeable() && object.uIsSet(feature)) {
                Object value = object.uGet(feature);

                if (feature instanceof UAttribute) {
                    UAttribute attribute = (UAttribute) feature;

                    writer.writeStartElement(ELEMENT_ATTRIBUTE);
                    writer.writeAttribute(
                            ATTRIBUTE_NAME, attribute.getName()
                    );
                    writer.writeCData(
                            attribute.getType().getConverter().convertToString(
                                    value
                            )
                    );
                    writer.writeEndElement();
                }
                else {
                    UReference reference = (UReference) feature;

                    writer.writeStartElement(ELEMENT_REFERENCE);
                    writer.writeAttribute(
                            ATTRIBUTE_NAME, reference.getName()
                    );

                    if (reference.isMany()) {
                        @SuppressWarnings(
                                "unchecked"
                        )
                        UList<UObject> referredObjects = (UList<UObject>) value;

                        for (UObject referredObject : referredObjects) {
                            writeReferredObject(referredObject);
                        }
                    }
                    else {
                        writeReferredObject(
                                (UObject) value
                        );
                    }

                    writer.writeEndElement();
                }
            }
        }

        writer.writeEndElement();
    }

    void writeReferredObject(UObject referredObject) throws UXMLException, XMLStreamException {
        String referredObjectID = lookup.getObjectID(referredObject);

        if (referredObjectID == null) {
            throw new UXMLException(
                    String.format(
                            "Object %s not found", referredObject
                    )
            );
        }

        writer.writeStartElement(ELEMENT_OBJECT_REF);
        writer.writeCharacters(referredObjectID);
        writer.writeEndElement();
    }

    public static void main(String[] args) throws Exception {
        StringWriter destination = new StringWriter();

        UXMLWriter uxmlWriter = new UXMLWriter();
        uxmlWriter.write(UModel.INSTANCE, destination);

        System.out.println(
                destination.toString()
        );
    }
}
